package com.shop.util;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.Configuration;

@Intercepts(value = { @Signature(args = { Connection.class,Integer.class }, method = "prepare", type = StatementHandler.class) })
public class PagingPlugin implements Interceptor {

	private Integer defaultPage;//默认页码
	private Integer defaultPageSize;//默认每页条数
	private Boolean defaultUseFlag;//默认是否启动插件
	private Boolean defaultCheckFlag;//默认是否检测当前页码的正确性
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		StatementHandler stmtHandler=getUnProxyObject(invocation);
		MetaObject metaStatementHandler = SystemMetaObject.forObject(stmtHandler);
		String sql=(String) metaStatementHandler.getValue("delegate.boundSql.sql");
		//不是select语句
		if( !checkSelect(sql) ){
			return invocation.proceed();
		}
		BoundSql boundSql=(BoundSql) metaStatementHandler.getValue("delegate.boundSql");
		Object parameterObject = boundSql.getParameterObject();
		PageParams pageParams=getPageParams(parameterObject);
		if (pageParams == null) { //没有分页参数，不启用插件
			return invocation.proceed();
		}
		//获取分页参数，获取不到时使用默认参数
		Integer pageNum=pageParams.getPage()==null?this.defaultPage:pageParams.getPage();
		Integer pageSize=pageParams.getPageSize()==null?this.defaultPageSize:pageParams.getPageSize();
		Boolean useFlag=pageParams.getUseFlag()==null?this.defaultUseFlag:pageParams.getUseFlag();
		Boolean checkFlag=pageParams.getCheckFlag()==null?this.defaultCheckFlag:pageParams.getCheckFlag();
		if (!useFlag) { //不使用分页插件
			return invocation.proceed();
		}
		int total=getTotal(invocation, metaStatementHandler, boundSql);
		//回填总数到分页参数里
		setTotalToPageParam(pageParams, total, pageSize);
		//检查当前页码的有效性
		checkPage(checkFlag, pageNum, pageParams.getTotalPage());
		//修改SQL
		return chanageSQL(invocation, metaStatementHandler, boundSql, pageNum    , pageSize);
	}

	@Override
	public Object plugin(Object statementHandler) {
		return Plugin.wrap(statementHandler, this);
	}

	@Override
	public void setProperties(Properties props) {
		this.defaultPage=Integer.parseInt(props.getProperty("default.page","1"));
		this.defaultPageSize=Integer.parseInt(props.getProperty("default.pageSize","5"));
		this.defaultUseFlag=Boolean.parseBoolean(props.getProperty("default.useFlag", "false"));
		this.defaultCheckFlag=Boolean.parseBoolean(props.getProperty("default.checkFlag", "false"));
	}
	/**
	 * 判断是否select语句
	 * @param sql
	 * @return 如果是select语句返回true否则false
	 */
	private boolean checkSelect(String sql) {
		String trimSql=sql.trim();
		int idx=trimSql.toLowerCase().indexOf("select");
		return idx == 0;
	}
	/**
	 * 检查当前页码的有效性
	 * @param checkFlag
	 * @param pageNum
	 * @param pageTotal
	 * @throws Throwable
	 */
	private void checkPage(Boolean checkFlag,Integer pageNum,Integer pageTotal) throws Throwable{
		if(checkFlag){
			//检查页面page是否合法
			if (pageNum > pageTotal) {
				throw new Exception("查询失败，查询页吗【"+pageNum+"】大于总页数【"+pageTotal+"】！");
			}
		}
	}
	/**
	 * 从代理对象中分离真实对象
	 * @param ivt --Invocation
	 * @return 非代理StatementHandler对象
	 */
	private StatementHandler getUnProxyObject(Invocation ivt) {
		StatementHandler statementHandler=(StatementHandler) ivt.getTarget();
		MetaObject metaStatementHandler=SystemMetaObject.forObject(statementHandler);
		//分离对象代理链(由于目标类可能被多个拦截器拦截，从而形成多次代理，通过循环可以分离出最原始的目标类)
		Object object = null;
		while(metaStatementHandler.hasGetter("h")) {
			object=metaStatementHandler.getValue("h");
		}
		if (object==null) {
			return statementHandler;
		}
		return (StatementHandler) object;
	}
	
	@SuppressWarnings("unchecked")
	private PageParams getPageParams(Object parameterObject) {
		if(parameterObject==null) return null;
		PageParams pageParams = null;
		//支持Map参数和MyBatis的@Param注解参数
		if (parameterObject instanceof Map) {
			Map<String,Object> paramMap=(Map<String, Object>) parameterObject;
			Set<String> keySet=paramMap.keySet();
			Iterator<String> iterator=keySet.iterator();
			while (iterator.hasNext()) {
				String key = iterator.next();
				Object value=paramMap.get(key);
				if(value instanceof PageParams) {
					return (PageParams) value;
				}
			}
		}else if(parameterObject instanceof PageParams){
			pageParams=(PageParams) parameterObject;
		}
		
		return pageParams;
	}
	/**
	 * 获取总数
	 * @param ivt
	 * @param metaStatementHandler
	 * @param boundSql
	 * @return 查询总数
	 * @throws Throwable 异常
	 */
	private int getTotal(Invocation ivt,MetaObject metaStatementHandler,BoundSql boundSql) throws Throwable{
		//获取当前的mappedStatement
		MappedStatement mappedStatement=(MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
		//配置对象
		Configuration cfg=mappedStatement.getConfiguration();
		//当前需要执行的SQL
		String sql=(String) metaStatementHandler.getValue("delegate.boundSql.sql");
		//改写为统计总数的SQL，这里是MySQL数据库，如果是其他数据库，需要按数据库SQL规范改写
		String countSql="select count(*) as total from ("+sql+") $_paging";
		//获取拦截方法参数，我们知道是Connection对象
		Connection conn=(Connection) ivt.getArgs()[0];
		PreparedStatement ps = null;
		int total = 0;
		try {
			ps=conn.prepareStatement(countSql);
			//构建统计总数BoundSql
			BoundSql countBoundSql=new BoundSql(cfg, countSql,boundSql.getParameterMappings(),boundSql.getParameterObject());
			//构建MyBatis的ParameterHandler用来设置总数SQL的参数
			ParameterHandler handler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), countBoundSql);
			//设置总数SQL参数
			handler.setParameters(ps);
			ResultSet rs = ps.executeQuery();
			while(rs.next()){
				total=rs.getInt("total");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			//这里不能关闭Connection，否则后续的SQL就没法继续了
			if (ps!=null) {
				ps.close();
			}
		}
		System.out.println("总条数:"+total);
		return total;
	}
	/**
	 * 设置总记录数和总页数
	 * @param pageParams
	 * @param total
	 * @param pageSize
	 */
	private void setTotalToPageParam(PageParams pageParams,int total,int pageSize){
		pageParams.setTotal(total);
		//计算总页数
		int totalPage=total%pageSize==0?total/pageSize:total/pageSize+1;
		pageParams.setTotalPage(totalPage);
	}
	/**
	 * 修改当前查询的SQL
	 * @param invocation
	 * @param metaStatementHandler
	 * @param boundSql
	 * @param page
	 * @param pageSize
	 * @return
	 * @throws Exception
	 */
	private Object chanageSQL(Invocation invocation,MetaObject metaStatementHandler,BoundSql boundSql,int page,int pageSize) throws Exception{
		//获取当前需要执行的SQL
		String sql=(String) metaStatementHandler.getValue("delegate.boundSql.sql");
		//修改SQL,这里使用的是MySQL,如果是其他数据库则需要修改
		String newSql="select * from ( "+sql+" ) $_paging_table limit ?,?";
		//修改当前需要执行的SQL
		metaStatementHandler.setValue("delegate.boundSql.sql", newSql);
		//相当于调用StatementHandler的prepare方法，预编译了当前SQL并设置原有的参数
		//但是少了两个分页参数，它返回的是一个PreparedStatement对象
		PreparedStatement ps=(PreparedStatement) invocation.proceed();
		//计算SQL总参数个数
		int count=ps.getParameterMetaData().getParameterCount();
		//设置两个分页参数
		ps.setInt(count-1, (page-1)*pageSize);
		ps.setInt(count, pageSize);
		return ps;
	}
}
